{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "origin_pos": 0
   },
   "source": [
    "# 序列模型\n",
    ":label:`sec_sequence`\n",
    "\n",
    "想象一下你正在看网飞（Netflix，一个国外的视频网站）上的电影。\n",
    "作为一名忠实的用户，你对每一部电影都给出评价，\n",
    "毕竟一部好电影需要更多的支持和认可。\n",
    "然而事实证明，事情并不那么简单。\n",
    "随着时间的推移，人们对电影的看法会发生很大的变化。\n",
    "事实上，心理学家甚至对这些现象起了名字：\n",
    "\n",
    "* *锚定*（anchoring）效应：基于其他人的意见做出评价。\n",
    "  例如，奥斯卡颁奖后，受到关注的电影的评分会上升，尽管它还是原来那部电影。\n",
    "  这种影响将持续几个月，直到人们忘记了这部电影曾经获得的奖项。\n",
    "  结果表明（ :cite:`Wu.Ahmed.Beutel.ea.2017`），这种效应会使评分提高半个百分点以上。\n",
    "* *享乐适应*（hedonic adaption）：人们迅速接受并且适应一种更好或者更坏的情况\n",
    "  作为新的常态。\n",
    "  例如，在看了很多好电影之后，人们会强烈期望下部电影会更好。\n",
    "  因此，在许多精彩的电影被看过之后，即使是一部普通的也可能被认为是糟糕的。\n",
    "* *季节性*（seasonality）：少有观众喜欢在八月看圣诞老人的电影。\n",
    "* 有时，电影会由于导演或演员在制作中的不当行为变得不受欢迎。\n",
    "* 有些电影因为其极度糟糕只能成为小众电影。*Plan9from Outer Space*和*Troll2*就因为这个原因而臭名昭著的。\n",
    "\n",
    "简而言之，电影评分决不是固定不变的。\n",
    "因此，使用时间动力学可以得到更准确的电影推荐 :cite:`Koren.2009`。\n",
    "当然，序列数据不仅仅是关于电影评分的。\n",
    "下面给出了更多的场景：\n",
    "\n",
    "* 在使用应用程序时，许多用户都有很强的特定习惯。\n",
    "  例如，在学生放学后社交媒体应用更受欢迎。在市场开放时股市交易软件更常用。\n",
    "* 预测明天的股价要比过去的股价更困难，尽管两者都只是估计一个数字。\n",
    "  毕竟，先见之明比事后诸葛亮难得多。\n",
    "  在统计学中，前者（对超出已知观测范围进行预测）称为*外推法*（extrapolation），\n",
    "  而后者（在现有观测值之间进行估计）称为*内插法*（interpolation）。\n",
    "* 在本质上，音乐、语音、文本和视频都是连续的。\n",
    "  如果它们的序列被我们重排，那么就会失去原有的意义。\n",
    "  比如，一个文本标题“狗咬人”远没有“人咬狗”那么令人惊讶，尽管组成两句话的字完全相同。\n",
    "* 地震具有很强的相关性，即大地震发生后，很可能会有几次小余震，\n",
    "  这些余震的强度比非大地震后的余震要大得多。\n",
    "  事实上，地震是时空相关的，即余震通常发生在很短的时间跨度和很近的距离内。\n",
    "* 人类之间的互动也是连续的，这可以从微博上的争吵和辩论中看出。\n",
    "\n",
    "## 统计工具\n",
    "\n",
    "处理序列数据需要统计工具和新的深度神经网络架构。\n",
    "为了简单起见，我们以 :numref:`fig_ftse100`所示的股票价格（富时100指数）为例。\n",
    "\n",
    "![近30年的富时100指数](https://d2l.ai/_images/ftse100.png)\n",
    ":width:`400px`\n",
    ":label:`fig_ftse100`\n",
    "\n",
    "其中，用$x_t$表示价格，即在*时间步*（time step）\n",
    "$t \\in \\mathbb{Z}^+$时，观察到的价格$x_t$。\n",
    "请注意，$t$对于本文中的序列通常是离散的，并在整数或其子集上变化。\n",
    "假设一个交易员想在$t$日的股市中表现良好，于是通过以下途径预测$x_t$：\n",
    "\n",
    "$$x_t \\sim P(x_t \\mid x_{t-1}, \\ldots, x_1).$$\n",
    "\n",
    "### 自回归模型\n",
    "\n",
    "为了实现这个预测，交易员可以使用回归模型，\n",
    "例如在 :numref:`sec_linear_concise`中训练的模型。\n",
    "仅有一个主要问题：输入数据的数量，\n",
    "输入$x_{t-1}, \\ldots, x_1$本身因$t$而异。\n",
    "也就是说，输入数据的数量这个数字将会随着我们遇到的数据量的增加而增加，\n",
    "因此需要一个近似方法来使这个计算变得容易处理。\n",
    "本章后面的大部分内容将围绕着如何有效估计\n",
    "$P(x_t \\mid x_{t-1}, \\ldots, x_1)$展开。\n",
    "简单地说，它归结为以下两种策略。\n",
    "\n",
    "第一种策略，假设在现实情况下相当长的序列\n",
    "$x_{t-1}, \\ldots, x_1$可能是不必要的，\n",
    "因此我们只需要满足某个长度为$\\tau$的时间跨度，\n",
    "即使用观测序列$x_{t-1}, \\ldots, x_{t-\\tau}$。\n",
    "当下获得的最直接的好处就是参数的数量总是不变的，\n",
    "至少在$t > \\tau$时如此，这就使我们能够训练一个上面提及的深度网络。\n",
    "这种模型被称为*自回归模型*（autoregressive models），\n",
    "因为它们是对自己执行回归。\n",
    "\n",
    "第二种策略，如 :numref:`fig_sequence-model`所示，\n",
    "是保留一些对过去观测的总结$h_t$，\n",
    "并且同时更新预测$\\hat{x}_t$和总结$h_t$。\n",
    "这就产生了基于$\\hat{x}_t = P(x_t \\mid h_{t})$估计$x_t$，\n",
    "以及公式$h_t = g(h_{t-1}, x_{t-1})$更新的模型。\n",
    "由于$h_t$从未被观测到，这类模型也被称为\n",
    "*隐变量自回归模型*（latent autoregressive models）。\n",
    "\n",
    "![隐变量自回归模型](https://d2l.ai/_images/sequence-model.svg)\n",
    ":label:`fig_sequence-model`\n",
    "\n",
    "这两种情况都有一个显而易见的问题：如何生成训练数据？\n",
    "一个经典方法是使用历史观测来预测下一个未来观测。\n",
    "显然，我们并不指望时间会停滞不前。\n",
    "然而，一个常见的假设是虽然特定值$x_t$可能会改变，\n",
    "但是序列本身的动力学不会改变。\n",
    "这样的假设是合理的，因为新的动力学一定受新的数据影响，\n",
    "而我们不可能用目前所掌握的数据来预测新的动力学。\n",
    "统计学家称不变的动力学为*静止的*（stationary）。\n",
    "因此，整个序列的估计值都将通过以下的方式获得：\n",
    "\n",
    "$$P(x_1, \\ldots, x_T) = \\prod_{t=1}^T P(x_t \\mid x_{t-1}, \\ldots, x_1).$$\n",
    "\n",
    "注意，如果我们处理的是离散的对象（如单词），\n",
    "而不是连续的数字，则上述的考虑仍然有效。\n",
    "唯一的差别是，对于离散的对象，\n",
    "我们需要使用分类器而不是回归模型来估计$P(x_t \\mid  x_{t-1}, \\ldots, x_1)$。\n",
    "\n",
    "### 马尔可夫模型\n",
    "\n",
    "回想一下，在自回归模型的近似法中，\n",
    "我们使用$x_{t-1}, \\ldots, x_{t-\\tau}$\n",
    "而不是$x_{t-1}, \\ldots, x_1$来估计$x_t$。\n",
    "只要这种是近似精确的，我们就说序列满足*马尔可夫条件*（Markov condition）。\n",
    "特别是，如果$\\tau = 1$，得到一个\n",
    "*一阶马尔可夫模型*（first-order Markov model），\n",
    "$P(x)$由下式给出：\n",
    "\n",
    "$$P(x_1, \\ldots, x_T) = \\prod_{t=1}^T P(x_t \\mid x_{t-1}) \\text{ 当 } P(x_1 \\mid x_0) = P(x_1).$$\n",
    "\n",
    "当假设$x_t$仅是离散值时，这样的模型特别棒，\n",
    "因为在这种情况下，使用动态规划可以沿着马尔可夫链精确地计算结果。\n",
    "例如，我们可以高效地计算$P(x_{t+1} \\mid x_{t-1})$：\n",
    "\n",
    "$$\n",
    "\\begin{aligned}\n",
    "P(x_{t+1} \\mid x_{t-1})\n",
    "&= \\frac{\\sum_{x_t} P(x_{t+1}, x_t, x_{t-1})}{P(x_{t-1})}\\\\\n",
    "&= \\frac{\\sum_{x_t} P(x_{t+1} \\mid x_t, x_{t-1}) P(x_t, x_{t-1})}{P(x_{t-1})}\\\\\n",
    "&= \\sum_{x_t} P(x_{t+1} \\mid x_t) P(x_t \\mid x_{t-1})\n",
    "\\end{aligned}\n",
    "$$\n",
    "\n",
    "利用这一事实，我们只需要考虑过去观察中的一个非常短的历史：\n",
    "$P(x_{t+1} \\mid x_t, x_{t-1}) = P(x_{t+1} \\mid x_t)$。\n",
    "隐马尔可夫模型中的动态规划超出了本节的范围\n",
    "（我们将在 :numref:`sec_bi_rnn`再次遇到），\n",
    "而动态规划这些计算工具已经在控制算法和强化学习算法广泛使用。\n",
    "\n",
    "### 因果关系\n",
    "\n",
    "原则上，将$P(x_1, \\ldots, x_T)$倒序展开也没什么问题。\n",
    "毕竟，基于条件概率公式，我们总是可以写出：\n",
    "\n",
    "$$P(x_1, \\ldots, x_T) = \\prod_{t=T}^1 P(x_t \\mid x_{t+1}, \\ldots, x_T).$$\n",
    "\n",
    "事实上，如果基于一个马尔可夫模型，\n",
    "我们还可以得到一个反向的条件概率分布。\n",
    "然而，在许多情况下，数据存在一个自然的方向，即在时间上是前进的。\n",
    "很明显，未来的事件不能影响过去。\n",
    "因此，如果我们改变$x_t$，可能会影响未来发生的事情$x_{t+1}$，但不能反过来。\n",
    "也就是说，如果我们改变$x_t$，基于过去事件得到的分布不会改变。\n",
    "因此，解释$P(x_{t+1} \\mid x_t)$应该比解释$P(x_t \\mid x_{t+1})$更容易。\n",
    "例如，在某些情况下，对于某些可加性噪声$\\epsilon$，\n",
    "显然我们可以找到$x_{t+1} = f(x_t) + \\epsilon$，\n",
    "而反之则不行 :cite:`Hoyer.Janzing.Mooij.ea.2009`。\n",
    "这是个好消息，因为这个前进方向通常也是我们感兴趣的方向。\n",
    "彼得斯等人写的这本书 :cite:`Peters.Janzing.Scholkopf.2017`\n",
    "已经解释了关于这个主题的更多内容，而我们仅仅触及了它的皮毛。\n",
    "\n",
    "## 训练\n",
    "\n",
    "在了解了上述统计工具后，让我们在实践中尝试一下！\n",
    "首先，我们生成一些数据：使用正弦函数和一些可加性噪声来生成序列数据，\n",
    "时间步为$1, 2, \\ldots, 1000$。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%load ../utils/djl-imports\n",
    "%load ../utils/plot-utils\n",
    "%load ../utils/Functions.java"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "NDManager manager = NDManager.newBaseManager();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "public static Figure plot(double[] x, double[] y, String xLabel, String yLabel) {\n",
    "    ScatterTrace trace = ScatterTrace.builder(x,y)\n",
    "        .mode(ScatterTrace.Mode.LINE)\n",
    "        .build();\n",
    "\n",
    "    Layout layout = Layout.builder()\n",
    "            .showLegend(true)\n",
    "            .xAxis(Axis.builder().title(xLabel).build())\n",
    "            .yAxis(Axis.builder().title(yLabel).build())\n",
    "            .build();\n",
    "\n",
    "    return new Figure(layout, trace);\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "int T = 1000; // 总共生成1000个点\n",
    "NDArray time = manager.arange(1f, T+1);\n",
    "NDArray x = time.mul(0.01).sin().add(\n",
    "    manager.randomNormal(0f, 0.2f, new Shape(T), DataType.FLOAT32));\n",
    "\n",
    "double[] xAxis = Functions.floatToDoubleArray(time.toFloatArray());\n",
    "double[] yAxis = Functions.floatToDoubleArray(x.toFloatArray());\n",
    "\n",
    "plot(xAxis, yAxis, \"time\", \"x\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "origin_pos": 6
   },
   "source": [
    "接下来，我们需要将这样一个序列转换为我们的模型可以训练的特征和标签。\n",
    "基于嵌入维度 $\\tau$ ，我们将数据映射到 $y_t = x_t$ 和$\\mathbf{x}_t = [x_{t-\\tau}, \\ldots, x_{t-1}]$ 对中。\n",
    "精明的读者可能已经注意到，这给我们提供了 $\\tau$ 更少的数据示例，因为我们没有足够的第一个 $\\tau$ 的历史记录。\n",
    "一个简单的修复，特别是如果序列很长，\n",
    "就是放弃这几个术语。\n",
    "或者我们可以用零填充序列。\n",
    "这里，我们仅使用前600个特征标签对进行训练。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "int tau = 4;\n",
    "NDArray features = manager.zeros(new Shape(T - tau, tau));\n",
    "\n",
    "for (int i = 0; i < tau; i++) {\n",
    "    features.set(new NDIndex(\":, {}\", i), x.get(new NDIndex(\"{}:{}\", i, T - tau + i)));\n",
    "}\n",
    "NDArray labels = x.get(new NDIndex(\"\" + tau + \":\")).reshape(new Shape(-1,1));"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "int batchSize = 16;\n",
    "int nTrain = 600;\n",
    "// 只有第一个“nTrain”示例用于训练\n",
    "ArrayDataset trainIter = new ArrayDataset.Builder()\n",
    "    .setData(features.get(new NDIndex(\":{}\", nTrain)))\n",
    "    .optLabels(labels.get(new NDIndex(\":{}\", nTrain)))\n",
    "    .setSampling(batchSize, true)\n",
    "    .build();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "origin_pos": 10
   },
   "source": [
    "在这里，我们使体系结构相当简单：\n",
    "只有两个完全连接层的MLP，ReLU激活和平方损耗。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "// 一个简单的MLP\n",
    "public static SequentialBlock getNet() {\n",
    "    SequentialBlock net = new SequentialBlock();\n",
    "    net.add(Linear.builder().setUnits(10).build());\n",
    "    net.add(Activation::relu);\n",
    "    net.add(Linear.builder().setUnits(1).build());\n",
    "    return net;\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "origin_pos": 14
   },
   "source": [
    "现在我们已经准备好训练模型了。下面的代码与前面章节中的培训循环基本相同，\n",
    "例如 :numref:`sec_linear_concise`.\n",
    "因此，我们将不深入探讨太多细节。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "// 我们在函数 \"train\" 之外添加此项，以便在笔记本中保留对训练对象的强引用（否则有时可能会关闭）\n",
    "Trainer trainer = null; "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "public static Model train(SequentialBlock net, ArrayDataset dataset, int batchSize, int numEpochs, float learningRate) \n",
    "    throws IOException, TranslateException {\n",
    "    // 平方误差\n",
    "    Loss loss = Loss.l2Loss();\n",
    "    Tracker lrt = Tracker.fixed(learningRate);\n",
    "    Optimizer adam = Optimizer.adam().optLearningRateTracker(lrt).build();\n",
    "    \n",
    "    DefaultTrainingConfig config = new DefaultTrainingConfig(loss)\n",
    "        .optOptimizer(adam) // 优化器（损失函数）\n",
    "        .optInitializer(new XavierInitializer(), \"\")\n",
    "        .addTrainingListeners(TrainingListener.Defaults.logging()); // 日志\n",
    "    \n",
    "    Model model = Model.newInstance(\"sequence\");\n",
    "    model.setBlock(net);\n",
    "    trainer = model.newTrainer(config);\n",
    "    \n",
    "    for (int epoch = 1; epoch <= numEpochs; epoch++) {\n",
    "        // 在数据集上迭代\n",
    "        for (Batch batch : trainer.iterateDataset(dataset)) {\n",
    "            // 更新损失率和评估器\n",
    "            EasyTrain.trainBatch(trainer, batch);\n",
    "\n",
    "            // 更新参数\n",
    "            trainer.step();\n",
    "\n",
    "            batch.close();\n",
    "        }\n",
    "        \n",
    "        // 在新epoch结束时重置训练和验证求值器\n",
    "        trainer.notifyListeners(listener -> listener.onEpoch(trainer));\n",
    "        System.out.printf(\"Epoch %d\\n\", epoch);\n",
    "        System.out.printf(\"Loss %f\\n\", trainer.getTrainingResult().getTrainLoss());\n",
    "        \n",
    "        \n",
    "    }\n",
    "    return model;\n",
    "}\n",
    "SequentialBlock net = getNet();\n",
    "Model model = train(net, trainIter, batchSize, 5, 0.01f);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "origin_pos": 18
   },
   "source": [
    "## 预测\n",
    "\n",
    "由于训练损失很小，我们希望我们的模型能够很好地工作。\n",
    "让我们看看这在实践中意味着什么。\n",
    "首先要检查的是模型能够预测下一个时间步发生的情况，\n",
    "即*提前一步预测*。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Translator translator = new NoopTranslator(null);\n",
    "Predictor predictor = model.newPredictor(translator);\n",
    "\n",
    "NDArray onestepPreds = ((NDList) predictor.predict(new NDList(features))).get(0);\n",
    "\n",
    "ScatterTrace trace = ScatterTrace.builder(Functions.floatToDoubleArray(time.toFloatArray()), \n",
    "                                          Functions.floatToDoubleArray(x.toFloatArray()))\n",
    "        .mode(ScatterTrace.Mode.LINE)\n",
    "        .name(\"data\")\n",
    "        .build();\n",
    "\n",
    "ScatterTrace trace2 = ScatterTrace.builder(Functions.floatToDoubleArray(time.get(new NDIndex(\"{}:\", tau)).toFloatArray()),\n",
    "                                           Functions.floatToDoubleArray(onestepPreds.toFloatArray()))\n",
    "        .mode(ScatterTrace.Mode.LINE)\n",
    "        .name(\"1-step preds\")\n",
    "        .build();\n",
    "\n",
    "Layout layout = Layout.builder()\n",
    "        .showLegend(true)\n",
    "        .xAxis(Axis.builder().title(\"time\").build())\n",
    "        .yAxis(Axis.builder().title(\"x\").build())\n",
    "        .build();\n",
    "\n",
    "new Figure(layout, trace, trace2);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "origin_pos": 20
   },
   "source": [
    "正如我们预期的那样，提前一步的预测看起来不错。\n",
    "即使超过604次（`n_train+tau`）观测，这些预测看起来仍然可信。\n",
    "然而，这里只有一个小问题：\n",
    "如果我们只在时间步604之前观察序列数据，我们就不可能希望收到所有未来一步预测的输入。\n",
    "相反，我们需要一步一步地向前迈进：\n",
    "\n",
    "$$\n",
    "\\hat{x}_{605} = f(x_{601}, x_{602}, x_{603}, x_{604}), \\\\\n",
    "\\hat{x}_{606} = f(x_{602}, x_{603}, x_{604}, \\hat{x}_{605}), \\\\\n",
    "\\hat{x}_{607} = f(x_{603}, x_{604}, \\hat{x}_{605}, \\hat{x}_{606}),\\\\\n",
    "\\hat{x}_{608} = f(x_{604}, \\hat{x}_{605}, \\hat{x}_{606}, \\hat{x}_{607}),\\\\\n",
    "\\hat{x}_{609} = f(\\hat{x}_{605}, \\hat{x}_{606}, \\hat{x}_{607}, \\hat{x}_{608}),\\\\\n",
    "\\ldots\n",
    "$$\n",
    "\n",
    "通常，对于高达 $x_t$ 的观测序列，其在时间步长 $t+k$ 处的预测输出 $\\hat{x}_{t+k}$ \n",
    "称为*$k$-步进预测*。\n",
    "由于我们观察到高达 $x_{604}$，其 $k$-步骤-预测为 $\\hat{x}_{604+k}$。\n",
    "换句话说，我们将不得不使用我们自己的预测来进行多步预测。\n",
    "让我们看看进展如何。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "NDArray multiStepPreds = manager.zeros(new Shape(T));\n",
    "multiStepPreds.set(new NDIndex(\":{}\", nTrain + tau), x.get(new NDIndex(\":{}\", nTrain + tau)));\n",
    "for (int i = nTrain + tau; i < T; i++) {\n",
    "    NDArray tempX = multiStepPreds.get(new NDIndex(\"{}:{}\", i - tau, i)).reshape(new Shape(1, -1));\n",
    "    NDArray prediction = ((NDList) predictor.predict(new NDList(tempX))).get(0);\n",
    "    multiStepPreds.set(new NDIndex(i), prediction);\n",
    "}\n",
    "\n",
    "ScatterTrace trace = ScatterTrace.builder(Functions.floatToDoubleArray(time.toFloatArray()), \n",
    "                                          Functions.floatToDoubleArray(x.toFloatArray()))\n",
    "        .mode(ScatterTrace.Mode.LINE)\n",
    "        .name(\"data\")\n",
    "        .build();\n",
    "\n",
    "ScatterTrace trace2 = ScatterTrace.builder(Functions.floatToDoubleArray(time.get(new NDIndex(\"{}:\", tau)).toFloatArray()),\n",
    "                                           Functions.floatToDoubleArray(onestepPreds.toFloatArray()))\n",
    "        .mode(ScatterTrace.Mode.LINE)\n",
    "        .name(\"1-step preds\")\n",
    "        .build();\n",
    "\n",
    "ScatterTrace trace3 = ScatterTrace.builder(Functions.floatToDoubleArray(time.get(\n",
    "                                                new NDIndex(\"{}:\", nTrain + tau)).toFloatArray()),\n",
    "                                           Functions.floatToDoubleArray(multiStepPreds.get(\n",
    "                                               new NDIndex(\"{}:\", nTrain + tau)).toFloatArray()))\n",
    "        .mode(ScatterTrace.Mode.LINE)\n",
    "        .name(\"multistep preds\")\n",
    "        .build();\n",
    "\n",
    "Layout layout = Layout.builder()\n",
    "        .showLegend(true)\n",
    "        .xAxis(Axis.builder().title(\"time\").build())\n",
    "        .yAxis(Axis.builder().title(\"x\").build())\n",
    "        .build();\n",
    "\n",
    "new Figure(layout, trace, trace2, trace3);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "origin_pos": 24
   },
   "source": [
    "正如上面的例子所示，这是一个惊人的失败。经过几个预测步骤后，预测很快衰减为常数。\n",
    "为什么算法工作得这么差？\n",
    "这最终是由于错误累积的事实。\n",
    "假设在第1步之后，出现了一些错误 $\\epsilon_1 = \\bar\\epsilon$。\n",
    "现在，步骤2的*输入*被 $\\epsilon_1$ 扰动，因此对于某些常量 $c$ ，\n",
    "我们会遇到一些顺序为 $\\epsilon_2 = \\bar\\epsilon + c \\epsilon_1$ 的错误，依此类推。\n",
    "误差可能很快偏离真实观测值。\n",
    "这是一种普遍现象。\n",
    "例如，未来24小时的天气预报往往相当准确，但除此之外，准确度会迅速下降。\n",
    "我们将在本章及以后讨论改进方法。\n",
    "\n",
    "让我们仔细看看 $k$-超前 预测中的困难\n",
    "通过计算对 $k = 1, 4, 16, 64$ 的整个序列的预测。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "int maxSteps = 64;\n",
    "\n",
    "NDArray features = manager.zeros(new Shape(T - tau - maxSteps + 1, tau + maxSteps));\n",
    "// 列 `i` (`i` < `tau`) 是从 `x` 观察到的来自\n",
    "// `i + 1` 到 `i + T - tau - maxSteps + 1`\n",
    "for (int i = 0; i < tau; i++) {\n",
    "    features.set(new NDIndex(\":, {}\", i), x.get(new NDIndex(\"{}:{}\", i, i + T - tau - maxSteps + 1)));\n",
    "}\n",
    "// 列 `i` (`i` >= `tau`) 是 (`i - tau + 1`) 的超前预测\n",
    "// 从 `i + 1` to `i + T - tau - maxSteps + 1` 的时间步长\n",
    "for (int i = tau; i < tau + maxSteps; i++) {\n",
    "    NDArray tempX = features.get(new NDIndex(\":, {}:{}\", i - tau, i));\n",
    "    NDArray prediction = ((NDList) predictor.predict(new NDList(tempX))).get(0);\n",
    "    features.set(new NDIndex(\":, {}\", i), prediction.reshape(-1));\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "int[] steps = new int[] {1, 4, 16, 64};\n",
    "\n",
    "ScatterTrace[] traces = new ScatterTrace[4];\n",
    "\n",
    "for (int i = 0; i < traces.length; i++) {\n",
    "    int step = steps[i];\n",
    "    traces[i] = ScatterTrace.builder(Functions.floatToDoubleArray(time.get(new NDIndex(\"{}:{}\", tau + step - 1, T - maxSteps + i)).toFloatArray()), \n",
    "                                     Functions.floatToDoubleArray(features.get(\n",
    "                                         new NDIndex(\":,{}\", tau + step - 1)).toFloatArray())\n",
    "                                    )\n",
    "                .mode(ScatterTrace.Mode.LINE)\n",
    "                .name(step + \"-step preds\")\n",
    "                .build();\n",
    "}\n",
    "\n",
    "\n",
    "Layout layout = Layout.builder()\n",
    "                .showLegend(true)\n",
    "                .xAxis(Axis.builder().title(\"time\").build())\n",
    "                .yAxis(Axis.builder().title(\"x\").build())\n",
    "                .build();\n",
    "\n",
    "new Figure(layout, traces);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "origin_pos": 29
   },
   "source": [
    "这清楚地说明了当我们试图预测未来时，预测的质量是如何变化的。\n",
    "尽管提前4步的预测看起来仍然不错，但超出这一步的任何预测几乎都是无用的。\n",
    "\n",
    "\n",
    "## 总结\n",
    "\n",
    "* 内插法和外推法在难度上有很大差别。因此，如果您有一个序列，那么在进行训练时，请始终遵守数据的时间顺序，即永远不要对未来的数据进行训练。\n",
    "* 序列模型需要专门的统计工具进行估计。两种流行的选择是自回归模型和潜变量自回归模型。\n",
    "* 对于因果模型（例如，时间向前），估计正向通常比反向容易得多。\n",
    "* 对于时间步 $t$ 之前的观测序列，其在时间步 $t+k$ 的预测输出为 $k$*-超前预测*。当我们通过增加 $k$ 来进一步预测时，误差会累积，预测质量会下降，通常会急剧下降。\n",
    "\n",
    "\n",
    "## 练习\n",
    "\n",
    "1. 在本部分的实验中对模型进行了改进。\n",
    "    1. 纳入超过过去4次的观察结果？你到底需要多少？\n",
    "    2. 如果没有noise，您需要多少过去的观察？提示：您可以将 $\\sin$ 和 $\\cos$ 写成微分方程。\n",
    "    3. 在保持特征总数不变的情况下，您能否合并旧的观察结果？这会提高准确性吗？为什么？\n",
    "    4. 改变神经网络结构并评估性能。\n",
    "2. 投资者希望找到一种好的证券来购买。他查看过去的回报，以决定哪一个可能表现良好。这种策略可能会出什么问题？\n",
    "3. 因果关系是否也适用于文本？到什么程度？\n",
    "4. 举例说明何时可能需要潜在自回归模型来捕获数据的动态。\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Java",
   "language": "java",
   "name": "java"
  },
  "language_info": {
   "codemirror_mode": "java",
   "file_extension": ".jshell",
   "mimetype": "text/x-java-source",
   "name": "Java",
   "pygments_lexer": "java",
   "version": "14.0.2+12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
